package com.event.starter.interceptor;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.event.common.annotation.RepoNameSpace;
import com.event.common.annotation.RepoTableId;
import com.event.common.enums.RepoIdTypeEnum;
import com.event.common.enums.UpdateTypeEnum;
import com.event.common.exception.UpdateEventException;
import com.event.common.model.ClassAnnotationEvent;
import com.event.common.model.UpdateAfterEvent;
import com.event.common.model.UpdateBeforeEvent;
import com.event.common.util.SqlParseUtil;
import com.event.core.context.AnnotationContext;
import com.event.core.context.EventContext;
import com.event.core.util.RepoSwitchContext;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 *  MybatisPlus拦截器(update,delete,insert拦截器(拦截monitorNamespace配置的命名空间))
 */
@Slf4j
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class MybatisUpdateInterceptor implements Interceptor {
    // 注解上下文
    private AnnotationContext annotationContext;
    // 事件上下文
    private EventContext eventContext;
    // 关注的类注解列表
    private List<ClassAnnotationEvent> classAnnotationEvents;
    // 注解事件map
    private Map<String,ClassAnnotationEvent> classAnnotationEventsMap;

    public MybatisUpdateInterceptor(AnnotationContext annotationContext, EventContext eventContext, Predicate<ClassAnnotationEvent> predicate){
        this.annotationContext = annotationContext;
        this.eventContext = eventContext;
        this.classAnnotationEvents = annotationContext.getClassAnnotationEvents(predicate);
        if(CollUtil.isEmpty(classAnnotationEvents)){
            return;
        }
        classAnnotationEventsMap = classAnnotationEvents.stream().collect(Collectors.toMap(item -> item.getClazz().getName(), item -> item,(k1,k2)-> k2));
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        if(ObjectUtil.isNull(args)){
            return invocation.proceed();
        }
        // 开关控制
        if(!RepoSwitchContext.get()){
            return invocation.proceed();
        }
        UpdateBeforeEvent beforeEvent = null;
        MappedStatement ms = null;
        try {
            ms = (MappedStatement) args[0];
            // 执行方法id（全路径类名+方法名）
            String msId = ms.getId();
            String namespace = msId.substring(0, msId.lastIndexOf("."));
            if(CollUtil.isEmpty(classAnnotationEventsMap)){
                return invocation.proceed();
            }
            // 注解事件收集
            ClassAnnotationEvent classAnnotationEvent = classAnnotationEventsMap.get(namespace);
            if(ObjectUtil.isNull(classAnnotationEvent)){
                return invocation.proceed();
            }

            // 前置事件
            RepoNameSpace repoNameSpace = (RepoNameSpace) classAnnotationEvent.getAnnotation();
            beforeEvent = new UpdateBeforeEvent();
            beforeEvent.setNamespace(ObjectUtil.defaultIfBlank(repoNameSpace.namespace(), namespace));
            beforeEvent.setAlias(repoNameSpace.alias());
            beforeEvent.setMapperClass(classAnnotationEvent.getClazz());
            beforeEvent.setModelClass(repoNameSpace.modelClass());
            beforeEvent.setUpdateType(getUpdateType(ms).toString());
            setIdInfo(ms, args[1], beforeEvent);
            postBeforeEvent(beforeEvent);
        }catch (UpdateEventException e){
            log.error("beforeEvent UpdateEventException", e);
        }
        catch (Exception e){
            log.error("intercept error", e);
        }
        // 业务执行
        Object obj = invocation.proceed();
        try {
            if(Objects.nonNull(beforeEvent)){
                postAfterEvent(ms,args[1],beforeEvent);
            }
        }catch (UpdateEventException e){
            log.error("afterEvent UpdateEventException", e);
        }catch (Exception e){
            log.error("intercept error", e);
        }
        return obj;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 获取操作类型
     * @param ms
     * @return
     */
    private UpdateTypeEnum getUpdateType(MappedStatement ms){
        switch (ms.getSqlCommandType()){
            case UPDATE:
                return UpdateTypeEnum.UPDATE;
            case INSERT:
                return UpdateTypeEnum.INSERT;
            case DELETE:
                return UpdateTypeEnum.DELETE;
            default:
                return null;
        }
    }

    /**
     * 执行前事件
     * @param beforeEvent
     */
    private void postBeforeEvent(UpdateBeforeEvent beforeEvent){
        long start = System.currentTimeMillis();
        eventContext.updateBeforeEvent(beforeEvent);
        log.info("======> UpdateBeforeEvent : 执行耗时：{}ms",System.currentTimeMillis() - start);
    }

    /**
     *
     * @param ms
     * @param param
     * @param beforeEvent
     */
    private void postAfterEvent(MappedStatement ms,Object param,UpdateBeforeEvent beforeEvent){
        UpdateAfterEvent afterEvent = new UpdateAfterEvent();
        BeanUtil.copyProperties(beforeEvent, afterEvent);
        // id值可能为空，需要重新获取
        if(StrUtil.isBlank(afterEvent.getIdValue()) && RepoIdTypeEnum.AUTO.getType().equals(afterEvent.getAutoIncr())){
            String idValue = parseIdValue(ms, param, beforeEvent);
            if(StrUtil.isBlank(idValue)){
                log.warn(beforeEvent.getAlias()+" idValue is null , sql : {} , param : {}",ms.getBoundSql(param).getSql(),JSONObject.toJSONString(param));
            }
            afterEvent.setIdValue(idValue);
        }
        long start = System.currentTimeMillis();
        eventContext.updateAfterEvent(afterEvent);
        log.info("======> UpdateAfterEvent : 执行耗时：{}ms",System.currentTimeMillis() - start);
    }

    /**
     * 设置id信息
     * @param ms
     * @param param
     * @param beforeEvent
     */
    private void setIdInfo(MappedStatement ms,Object param,UpdateBeforeEvent beforeEvent){
        Field idField = getIdField(beforeEvent.getModelClass());
        RepoTableId repoTableId = idField.getAnnotation(RepoTableId.class);
        beforeEvent.setIdName(idField.getName());
        beforeEvent.setDbIdName(repoTableId.dbIdName());
        beforeEvent.setAutoIncr(repoTableId.autoIncrement());
        beforeEvent.setIdValue(parseIdValue(ms, param, beforeEvent));
    }

    /**
     * 获取id值
     * @param ms
     * @param param
     * @param beforeEvent
     * @return
     */
    private String parseIdValue(MappedStatement ms,Object param,UpdateBeforeEvent beforeEvent){
        BoundSql boundSql = ms.getBoundSql(param);
        try {
            String sql = getFinalSql(boundSql);
            String idValue = SqlParseUtil.getUpdateValue(CCJSqlParserUtil.parse(sql), beforeEvent.getDbIdName());
            return idValue;
        }catch (Exception e){
            log.error("parseIdValue error", e);
            throw new UpdateEventException("parseIdValue error");
        }
    }

    private static String getFinalSql(BoundSql boundSql) {
        String sql = boundSql.getSql();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        Object parameterObject = boundSql.getParameterObject();

        MetaObject metaObject = parameterObject == null ? null : SystemMetaObject.forObject(parameterObject);

        for (ParameterMapping parameterMapping : parameterMappings) {
            String propertyName = parameterMapping.getProperty();
            Object value = metaObject == null ? null : metaObject.getValue(propertyName);
            if (value == null) {
                value = parameterMapping.getJavaType().getTypeName();
            }
            sql = sql.replaceFirst("\\?", value.toString());
        }

        return sql;
    }

    /**
     * 获取id字段
     * @param mapperClass
     * @return
     */
    private Field getIdField(Class<?> mapperClass){
        Optional<Field> optional = Arrays.stream(ReflectUtil.getFields(mapperClass)).filter(field -> field.isAnnotationPresent(RepoTableId.class)).findFirst();
        if(!optional.isPresent()){
            throw new UpdateEventException("Not Field has RepoTableId");
        }
        return optional.get();
    }
}
